package org.dfl.core.model.transitions;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FilenameUtils;
import org.dfl.core.data.types.Type;
import org.dfl.core.data.types.TypeManager;
import org.dfl.core.model.TInputEdge;
import org.dfl.core.model.TOutputEdge;
import org.dfl.core.model.TransitionExecutionException;
import org.dfl.core.model.XMLLoaderException;
import org.dfl.core.utils.JarUtils;
import org.dfl.messages.Messages;
import org.jdom.Element;

/**
 * transition that loads jars and fires static method in a given loaded class from that jars
 * @author Piotr Wlodarczyk
 *
 */
public class JarTransition extends FixedInputsTransition {
	private String[] jarList = {};
	
	private String dir;
	private String packageStr;
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	private Class c;
	private Method m;
	
	public String getClassName() {
		if(c==null) return ""; //$NON-NLS-1$
		return JarUtils.getClassNameFromClassStr(c.getName());
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	public void setClassName(String className) throws Exception{
		if(className==null || className.equals("")){ //$NON-NLS-1$
			reset();
			c = null;
			m = null;
			return;
		}
		
		String fullClassName = (getPackageStr().equals("")?className:(getPackageStr()+"."+className)); //$NON-NLS-1$ //$NON-NLS-2$
		setFullClassName(fullClassName);
	}
	
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	public void setFullClassName(String className) throws Exception {
		if (!className.equals(getFullClassName())) {
			String[] jars = new String[jarList.length];
			for(int i=0; i<jarList.length;i++){
				jars[i]=FilenameUtils.separatorsToSystem(dir==null?jarList[i]:(dir+"/"+jarList[i]));
			}
			URLClassLoader ucl = JarUtils.getClassLoader(jars);
			Class c = ucl.loadClass(className);
			reset();
			packageStr = JarUtils.getPackageNameFromClassStr(className);
			this.c = c;
			m = null;
		}
	}
			
	public String getFullClassName(){
		if(c==null) return ""; //$NON-NLS-1$
		return c.getName();
	}
	
	public void setPackageStr(String packageStr) {
		if(!packageStr.equals(this.packageStr)){
			this.packageStr = packageStr;
			try {
				setClassName(null);
			} catch (Exception e) {
			}
		}
	}

	public String getPackageStr() {
		return packageStr;
	}
	
	private void reset() {
		disconnectAllInputs();
		disconnectAllOutputs();
		ArrayList<String> definitions = new ArrayList<String>(getInputsDefinition().keySet());
		for(String inputKey : definitions){
			removeInputDefinition(inputKey);
		}
	}

	public String[] getJarList() {
		return jarList;
	}

	public void setJarList(String[] jarList) {
		this.jarList = jarList;
	}
	
	public String getJarListStr(){
		String[] l = getJarList();
		StringBuffer res = new StringBuffer();
		String dot = ""; //$NON-NLS-1$
		for(String j:l){
			res.append(dot);
			res.append(j);
			dot = "\n"; //$NON-NLS-1$
		}
		return res.toString();
	}
	
	public void setJarListStr(String str){
		jarList = str.split("\n"); //$NON-NLS-1$
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	public List<String> getAvailableMethods(){
		List<String> res = new ArrayList<String>();
		Class c = getTargetClass();
		if(c!=null){
			List<Method> methods = JarUtils.getClassStaticMethods(getTargetClass());
			for(Method m : methods){
				res.add(getMethodStr(m));
			}
		}
		return res;
	}
	
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	private String getMethodStr(Method m){
		StringBuffer methodStr = new StringBuffer();
		methodStr.append(m.getName());
		methodStr.append("("); //$NON-NLS-1$

		// define inputs
		Class[] parametersTypes = m.getParameterTypes();
		for(int i=0;i<parametersTypes.length;i++){
			if(i>0) methodStr.append(", "); //$NON-NLS-1$
			methodStr.append(JarUtils.getClassNameFromClassStr(parametersTypes[i].getName())); //$NON-NLS-1$
		}
		methodStr.append(")->"); //$NON-NLS-1$
		
		// define outputs
		methodStr.append(JarUtils.getClassNameFromClassStr(m.getReturnType().getName()));
		return methodStr.toString();
	}
	
	public String getMethodStr(){
		if(this.m == null) return ""; //$NON-NLS-1$
		return getMethodStr(this.m);
	}
	
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	public void setMethodStr(String methodStr) throws Exception{
		if(this.c!=null && methodStr!=null && !methodStr.equals("")){ //$NON-NLS-1$
			Method[] methods = this.c.getMethods();
			for(Method m : methods){
				if(getMethodStr(m).equals(methodStr)){
					if(this.m != m){
						reset();
						
						Class returnType = m.getReturnType();
						setOutputType(getDataType(returnType));
						
						Class[] parametersTypes = m.getParameterTypes();
						for(int i=0;i<parametersTypes.length;i++){
							addInputDefinition("param"+(i+1), getDataType(parametersTypes[i])); //$NON-NLS-1$
						}
					}
					this.m = m;
					break;
				}
			}
		}else{
			m = null;
		}
	}
	
	@SuppressWarnings("unchecked") //$NON-NLS-1$
	private Class getTargetClass(){
		return c;
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	private Type getDataType(Class c){
		if(c == Map.class)  return TypeManager.getMapType();
		if(c == Collection.class)  return TypeManager.getCollectionType();
		if(c == Boolean.class || c == boolean.class) return TypeManager.getBooleanType();
		if(c == byte[].class) return TypeManager.getBinaryType();
		if(c == String.class)  return TypeManager.getStringType();

		// check interfaces
		java.lang.reflect.Type[] interfaces = c.getGenericInterfaces();
		if(interfaces!=null)
		for(java.lang.reflect.Type t :interfaces){
			if(t==Map.class) return TypeManager.getMapType();
		}
		for(java.lang.reflect.Type t :interfaces){
			if(t==Collection.class) return TypeManager.getCollectionType();
		}
		
		// TODO anyType or exception?!
		return TypeManager.getAnyType();
	}

	@Override
	protected void initInputDefinitions() {
	}

	@SuppressWarnings("unchecked") //$NON-NLS-1$
	@Override
	protected Object fire(Map<String, Object> inputMap) throws TransitionExecutionException {
		if(!isDefined()){
			throw new TransitionExecutionException(Messages.JarTransition_19);
		}

		try{
			Object instance = Modifier.isStatic(m.getModifiers())?null:c.newInstance();
			int paramLen = m.getParameterTypes().length;
			Object[] params = new Object[paramLen];
			for(int i=0;i<paramLen;i++){
				params[i] = inputMap.get("param"+(i+1)); //$NON-NLS-1$
			}
			return m.invoke(instance, params);
		}catch(Exception e){
			throw new TransitionExecutionException(e);
		}
	}

	@Override
	public String getDefaultDescription() {
		return Messages.JavaInvokeTransition_description;
	}

	@Override
	protected Type getDefaultOutputDefinition() {
		return TypeManager.getAnyType();
	}
	
	@Override
	public boolean isUserInitationNeeded() {
		return !isDefined();
	}
	
	@Override
	public void addInput(TInputEdge e) throws Exception {
		if(!isDefined()){
			throw new Exception(Messages.JavaInvokeTransition_errorDefineTransitionFirst);
		}
		super.addInput(e);
	}
	
	@Override
	public boolean addOutput(TOutputEdge e) throws Exception {
		if(!isDefined()){
			throw new Exception(Messages.JavaInvokeTransition_errorDefineTransitionFirst);
		}
		return super.addOutput(e);
	}

	private boolean isDefined() {
		return !getInputsDefinition().isEmpty();
	}
	

	@Override
	public void load(Element elem) throws XMLLoaderException {
		try {
			Element toolspecificElem = elem.getChild("toolspecific"); //$NON-NLS-1$
			load(elem,toolspecificElem.getChildText("dir",Transition.toolspecificNS));		
		} catch (Exception e) {
			throw new XMLLoaderException(e);
		}
	}
	
	public void load(Element elem,String dir) throws XMLLoaderException {
		super.load(elem);
		if(elem.getChild("toolspecific") == null){ //$NON-NLS-1$
			return;
		}
		try {
			Element toolspecificElem = elem.getChild("toolspecific"); //$NON-NLS-1$
			setDir(dir); //$NON-NLS-1$
			setJarListStr(toolspecificElem.getChildText("jars",Transition.toolspecificNS)); //$NON-NLS-1$
			try{
				setFullClassName(toolspecificElem.getChildText("class",Transition.toolspecificNS)); //$NON-NLS-1$
				setMethodStr(toolspecificElem.getChildText("method",Transition.toolspecificNS)); //$NON-NLS-1$
			}catch(Throwable e){
				throw new XMLLoaderException("Could not load jar transition.",e);
			}
		} catch (Exception e) {
			throw new XMLLoaderException(e);
		}
	}
	
	@Override
	public Element save() {
		Element elem = super.save();
		
		Element toolspecificElem = elem.getChild("toolspecific"); //$NON-NLS-1$
		if(toolspecificElem == null){
			toolspecificElem = new Element("toolspecific"); //$NON-NLS-1$
			toolspecificElem.setAttribute("tool",""); //$NON-NLS-1$ //$NON-NLS-2$
			toolspecificElem.setAttribute("version",""); //$NON-NLS-1$ //$NON-NLS-2$
			elem.addContent(toolspecificElem);
		}
		
		Element jarsElem = new Element("jars",Transition.toolspecificNS); //$NON-NLS-1$
		jarsElem.setText(getJarListStr());
		toolspecificElem.addContent(jarsElem);
		
		Element classElem = new Element("class",Transition.toolspecificNS); //$NON-NLS-1$
		classElem.setText(getFullClassName());
		toolspecificElem.addContent(classElem);
		
		Element methodElem = new Element("method",Transition.toolspecificNS); //$NON-NLS-1$
		methodElem.setText(getMethodStr());
		toolspecificElem.addContent(methodElem);
		
		Element dirElem = new Element("dir",Transition.toolspecificNS); //$NON-NLS-1$
		dirElem.setText(getDir());
		toolspecificElem.addContent(dirElem);
		return elem;
	}

	public String getDir() {
		return dir;
	}

	public void setDir(String dir) {
		this.dir = dir;
	}
}
